Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13장. 동기 호출 기반 복원력

6장에서 우리는 서비스 간 통신 방식을 다루었다.
동기 호출과 비동기 이벤트는 각각 장단점이 있다.

9장부터 12장까지는 이벤트 기반 구조를 깊이 있게 살펴보았다.
그렇다면 이런 질문이 자연스럽게 떠오른다.

그럼 모든 통신을 비동기로 만들면 되지 않을까?

현실은 그렇지 않다.

마이크로서비스는 모든 것을 비동기로 처리할 수 없다.
일부 흐름은 반드시 “즉시 응답”을 요구한다.

예를 들어:

  • 로그인 요청 → 토큰 발급 후 즉시 응답
  • 결제 승인 요청 → 승인 여부 즉시 반환
  • 관리자 화면 조회 → 여러 서비스에서 데이터 모아 즉시 반환

이런 흐름은 이벤트로 나중에 처리할 수 없다.
동기 호출은 현실적으로 피할 수 없는 선택이다.

문제는 이것이다.

동기 구조에서는 장애가 빠르게 전파된다.

그래서 동기 호출에는 별도의 복원력 설계가 필요하다.


동기 호출에서 장애가 전파되는 방식

다음과 같은 구조를 생각해보자.

flowchart LR
    Client --> Gateway --> Order
    Order --> Payment
    Payment --> PG

PG가 느려지면 어떤 일이 벌어질까?

  • Payment는 응답을 기다린다.
  • Order는 Payment를 기다린다.
  • Gateway는 Order를 기다린다.
  • Client는 응답을 기다린다.

작은 지연이 연쇄적으로 확산된다.

이것이 장애 전파(Failure Propagation) 다.

동기 구조의 문제는 단순하다.

기다림이 길어질수록 자원이 점유되고,
자원이 고갈되면 전체가 멈춘다.


동기 복원력의 목표

동기 호출 복원력의 목표는 다음과 같다.

  1. 무한 대기를 막는다.
  2. 실패를 빠르게 감지한다.
  3. 장애가 다른 기능으로 번지지 않게 한다.
  4. 전체 중단 대신 부분 기능 유지 상태를 만든다.

이를 위해 몇 가지 기본 전략이 필요하다.

1️⃣ Timeout — 기다림에 상한을 둬라

Timeout은 가장 기본이지만 가장 중요하다.

Timeout이 없으면:

  • 요청이 계속 대기
  • 스레드/이벤트 루프 점유
  • 커넥션 풀 고갈
  • 정상 요청까지 실패

느림은 결국 전체 장애로 이어진다.

동기 호출에는 반드시 적절한 Timeout이 있어야 한다.

그리고 중요한 점은:

외부 API뿐 아니라 브로커 전송 같은 외부 I/O도 동일하게 Timeout 대상이다.

다만 이벤트 기반 구조의 장애 양상은 다르므로,
비동기 구조는 다음 장에서 별도로 다룬다.

2️⃣ Retry — 조심해서 사용하라

재시도는 유용하지만 위험하다.

일시적인 네트워크 오류는 재시도로 해결될 수 있다.
하지만 상대 서비스가 이미 과부하 상태라면?

모든 호출자가 재시도를 시작하면
트래픽은 기하급수적으로 증가한다.

이를 Retry Storm라고 한다.

따라서 재시도에는 규칙이 필요하다.

  • 최대 횟수 제한
  • 지수 백오프(Exponential Backoff)
  • 랜덤 지터(Jitter)
  • 멱등한 요청만 재시도

재시도는 자동 복구 도구이지,
만능 해결책이 아니다.

3️⃣ Circuit Breaker — 고장 난 의존성은 잠시 끊어라

계속 실패하는 서비스를 계속 호출하면
내 서비스도 같이 죽는다.

Circuit Breaker는 다음과 같이 동작한다.

  • 실패율이 일정 기준을 넘으면 호출 차단(Open)
  • 일정 시간 후 일부 요청만 허용(Half-Open)
  • 회복되면 다시 정상 상태(Closed)

이 방식은 두 가지를 보호한다.

  • 장애가 난 서비스
  • 호출자 서비스

Circuit Breaker는
장애 확산을 막는 차단기다.

4️⃣ Bulkhead — 자원을 구획으로 나눠라

많은 사람들이 오해하는 부분이다.

“서비스가 분리되어 있는데 왜 또 격리가 필요한가?”

Bulkhead는 서비스 격리가 아니라
자원 격리다.

한 서비스 내부에서도 자원은 공유된다.

예:

  • 외부 API 호출
  • 내부 DB 조회
  • 캐시 접근

외부 API가 느려지면
그 호출을 처리하던 스레드와 커넥션이 점유된다.

결국 내부 기능까지 영향을 받을 수 있다.

Bulkhead 전략은:

  • 외부 호출 전용 스레드 풀 분리
  • 기능별 자원 풀 분리
  • 중요한 기능을 별도 워커로 격리

한쪽이 폭주해도
다른 쪽이 살아남도록 만드는 것이다.

5️⃣ Fail Fast — 느린 실패는 더 위험하다

동기 구조에서 가장 위험한 것은
“조금씩 느려지다가 결국 멈추는 상황”이다.

이미 장애가 명확하다면:

  • 빠르게 실패를 반환하고
  • 자원을 보호하는 것이 낫다.

Fail Fast는 포기가 아니라
시스템을 살리는 전략이다.

6️⃣ Graceful Degradation — 최소 기능 유지

완전한 정상 상태를 유지하지 못하더라도
부분 기능을 유지할 수 있다.

예:

  • 추천 서비스 장애 → 기본 목록 제공
  • 일부 상세 정보 실패 → 캐시 데이터 제공
  • 외부 통계 실패 → 이전 데이터 사용

복원력은 단순히 “살아있는 것”이 아니라
“어떻게 살아있게 할 것인가”의 문제다.


동기 복원력 설계 체크리스트

동기 호출이 있는 모든 지점에서 다음을 점검해야 한다.

  1. Timeout이 설정되어 있는가?
  2. Retry는 제한되어 있는가?
  3. Circuit Breaker가 있는가?
  4. 자원은 격리되어 있는가? (Bulkhead)
  5. 실패 시 빠르게 포기하는가? (Fail Fast)
  6. 대체 동작이 준비되어 있는가?

이 질문에 답하지 못하면
복원력은 설계되지 않은 것이다.


이 장의 핵심

  • 마이크로서비스는 모든 것을 비동기로 만들 수 없다.
  • 동기 호출은 현실적으로 필요하다.
  • 동기 구조에서는 장애가 빠르게 전파된다.
  • 복원력은 “기다림을 제한하고, 번짐을 막는 것”이다.
  • Timeout, Retry, Circuit Breaker, Bulkhead, Fail Fast는 필수 전략이다.
  • 완전한 정상보다 부분 동작 유지가 더 중요할 수 있다.